/*
 * Copyright 2024 ywcai
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

`include "defines.v"
`timescale 1ns/1ps

module l2cache(
	input	wire					clk,
	input	wire					rst_n,
	input	wire					i_missing_req_i,
	input	wire[`MemAddrBus]		i_addr_i,
	input	wire[7:0]				i_bytes_req_i,
	input	wire[7:0]				i_bytes_offset_i,
	input	wire					d_mem_req_i,
	input	wire					d_we_i,
	input	wire[`MemAddrBus]		d_addr_i,
	input	wire[7:0]				d_bytes_req_i,
	input	wire[7:0]				d_bytes_offset_i,
	input	wire[`CacheBus]			d_cache_wr_i,
	input	wire[`CacheBus]			rd_data_i,
	input	wire					transact_done_i,

	output	wire[`CacheBus]			i_data_l2_o,
	output	wire[`CacheBus]			d_data_l2_o,
	output	wire					i_req_done_o,
	output	wire					d_req_done_o,
	output	reg						mem_req_o,
	output	reg						we_o,
	output	reg[`MemAddrBus]		addr_o,
	output	reg[`CacheBus]  		wr_data_o,
	output	reg[`CACHE_BITS/8-1:0]	wr_strb_o,
	output	reg[11:0]				pulse_bits_o
	);

	localparam IDLE		= 6'b000001;
	localparam MATCH	= 6'b000010;
	localparam COMM		= 6'b000100;
	localparam FLUSH	= 6'b001000;
	localparam WRITE	= 6'b010000;
	localparam DONE		= 6'b100000;

	reg[4:0]			set_idx;

	//--------------------------------------------------------------------------
	//	{valid, dirty, tag, data}
	reg					en_1;
	reg					we_1;
	wire[542:0]			rdata_bank_1;
	reg[542:0]			wdata_bank_1;

	gen_cache bank_1(
		.clk(clk),
		.rst_n(rst_n),
		.en_i(en_1),
		.addr_i(set_idx),
		.we_i(we_1),
		.wdata_i(wdata_bank_1),
		.rdata_o(rdata_bank_1)
	);

	reg					en_2;
	reg					we_2;
	wire[542:0]			rdata_bank_2;
	reg[542:0]			wdata_bank_2;

	gen_cache bank_2(
		.clk(clk),
		.rst_n(rst_n),
		.en_i(en_2),
		.addr_i(set_idx),
		.we_i(we_2),
		.wdata_i(wdata_bank_2),
		.rdata_o(rdata_bank_2)
	);

	reg					en_3;
	reg					we_3;
	wire[542:0]			rdata_bank_3;
	reg[542:0]			wdata_bank_3;

	gen_cache bank_3(
		.clk(clk),
		.rst_n(rst_n),
		.en_i(en_3),
		.addr_i(set_idx),
		.we_i(we_3),
		.wdata_i(wdata_bank_3),
		.rdata_o(rdata_bank_3)
	);

	reg					en_4;
	reg					we_4;
	wire[542:0]			rdata_bank_4;
	reg[542:0]			wdata_bank_4;

	gen_cache bank_4(
		.clk(clk),
		.rst_n(rst_n),
		.en_i(en_4),
		.addr_i(set_idx),
		.we_i(we_4),
		.wdata_i(wdata_bank_4),
		.rdata_o(rdata_bank_4)
	);

	//--------------------------------------------------------------------------
	reg[1:0]			lru[31:0];
	reg[7:0]			flat_lru;
	reg[1:0]			cur_lru;
	reg[5:0]			cur_state, next_state;
	integer				loop;
	reg					ls_going;
	reg[1:0]			cur_going;
	reg					req_done;
	reg					flushing, flushed;
	reg[`CacheBus]		data_l2_o;
	reg[3:0]			section_count;
	reg[3:0]			section_start;
	reg[`MemAddrBus]	addr;
	reg					we;
	reg[28:0]			tag_addr;
	reg[11:0]			bytes_req;
	reg					hit_way1;
	reg					hit_way2;
	reg					hit_way3;
	reg					hit_way4;
	reg[1:0]			queue;

	wire[4:0] i_set_idx = i_addr_i[10:6];
	wire[28:0] i_tag_addr = i_addr_i[39:11];
//	wire[2:0] i_offset = i_addr_i[5:3];

	wire[4:0] d_set_idx = d_addr_i[10:6];
	wire[28:0] d_tag_addr = d_addr_i[39:11];
//	wire[2:0] d_offset = d_addr_i[5:3];

	wire[3:0] i_section_count = i_bytes_req_i >> 3;
	wire[3:0] i_section_start = i_bytes_offset_i >> 3;
	wire[3:0] d_section_count = d_bytes_req_i >> 3;
	wire[3:0] d_section_start = d_bytes_offset_i >> 3;

	// decoding
	assign i_data_l2_o = {`CACHE_BITS{cur_going == 2'b01}} & data_l2_o;
	assign d_data_l2_o = {`CACHE_BITS{cur_going == 2'b10}} & data_l2_o;
	assign i_req_done_o = (cur_going == 2'b01) & req_done;
	assign d_req_done_o = (cur_going == 2'b10) & req_done;

    initial begin
		for (loop = 0; loop < 32; loop = loop + 1) begin
			lru[loop] = 2'b00;
		end
    end

	function[1:0] lru_gen(input [7:0] lru);
		reg[1:0] idx1, idx2;
		reg[1:0] val1, val2;
		begin
			idx1 = (lru[1:0] <= lru[3:2]) ? 2'b00 : 2'b01;
			val1 = (lru[1:0] <= lru[3:2]) ? lru[1:0] : lru[3:2];
			idx2 = (lru[5:4] <= lru[7:6]) ? 2'b10 : 2'b11;
			val2 = (lru[5:4] <= lru[7:6]) ? lru[5:4] : lru[7:6];
			lru_gen = (val1 <= val2) ? idx1 : idx2;
		end
	endfunction

	always @(posedge clk) begin
	    if (rst_n == `RESET_ENABLE) begin
			cur_state <= IDLE;
		end else begin
			cur_state <= next_state;
		end
	end

	// write-back schema to memory
	// icache and dcache at the same time, icache favorite
	always @(posedge clk) begin
		if (rst_n == `RESET_ENABLE) begin
			next_state = IDLE;
			data_l2_o = `CACHE_BITS'h0;
			cur_going = 2'b00;
			flushing = `DISABLE;
			flushed = `DISABLE;
			req_done = `DISABLE;
			set_idx = 5'h0;
			addr = `MEM_ADDR_WIDTH'h0;
			we = `DISABLE;
			tag_addr = 29'h0;
			hit_way1 = `DISABLE;
			hit_way2 = `DISABLE;
			hit_way3 = `DISABLE;
			hit_way4 = `DISABLE;
			bytes_req = 12'h0;
			pulse_bits_o = 12'h200;
			section_count = 4'h0;
			section_start = 4'h0;
			queue = 2'b00;
			cur_lru = 2'b00;
			flat_lru = 8'h0;
			en_1 = `DISABLE;
			en_2 = `DISABLE;
			en_3 = `DISABLE;
			en_4 = `DISABLE;
			we_1 = `DISABLE;
			we_2 = `DISABLE;
			we_3 = `DISABLE;
			we_4 = `DISABLE;
		end else begin
			if (i_missing_req_i) begin
				queue[0] = `ENABLE;
			end
			if (d_mem_req_i) begin
				queue[1] = `ENABLE;
			end

			case (cur_state)
				IDLE: begin
					req_done = `DISABLE;
					if (queue[0]) begin
						// need remember something to be used later since the request only durate one cycle
						cur_going = 2'b01;
						set_idx = i_set_idx;
						addr = i_addr_i;
						we = `DISABLE;
						tag_addr = i_tag_addr;
						bytes_req = {4'h0, i_bytes_req_i};
						section_count = i_section_count;
						section_start = i_section_start;
						en_1 = `ENABLE;
						en_2 = `ENABLE;
						en_3 = `ENABLE;
						en_4 = `ENABLE;
						next_state = MATCH;
					end else if (queue[1]) begin
						cur_going = 2'b10;
						set_idx = d_set_idx;
						addr = d_addr_i;
						we = d_we_i;
						tag_addr = d_tag_addr;
						bytes_req = {4'h0, d_bytes_req_i};
						section_count = d_section_count;
						section_start = d_section_start;
						en_1 = `ENABLE;
						en_2 = `ENABLE;
						en_3 = `ENABLE;
						en_4 = `ENABLE;
						next_state = MATCH;
					end else begin
						cur_going = 2'b00;
					end
				end

				MATCH: begin
//					flat_lru = {lru[set_idx][3], lru[set_idx][2]
//						, lru[set_idx][1], lru[set_idx][0]};
//					cur_lru = lru_gen(flat_lru);
					cur_lru = lru[set_idx];

					hit_way1 = (tag_addr == rdata_bank_1[540:512]) && rdata_bank_1[542];
					hit_way2 = (tag_addr == rdata_bank_2[540:512]) && rdata_bank_2[542];
					hit_way3 = (tag_addr == rdata_bank_3[540:512]) && rdata_bank_3[542];
					hit_way4 = (tag_addr == rdata_bank_4[540:512]) && rdata_bank_4[542];

					flushing = `DISABLE;
					flushed = `DISABLE;
					if (!hit_way1 & !hit_way2
						& !hit_way3 & !hit_way4) begin
						mem_req_o = `ENABLE;
						addr_o = addr;
						we_o = `DISABLE;
						pulse_bits_o = bytes_req << 3;
						next_state = COMM;
					end else if (we) begin
						next_state = WRITE;
					end
				end

				COMM: begin
					if (transact_done_i) begin
						if (flushing) begin
							if (!we) begin
								if (cur_lru == 2'b00) begin
									for (loop = 0; loop < section_count; loop = loop + 1) begin
										wdata_bank_1[(loop+section_start)<<6 +: 64] =
											data_l2_o[(loop+section_start)<<6 +: 64];
									end
									wdata_bank_1[540:512] = tag_addr;
									wdata_bank_1[541] = `DISABLE;
									wdata_bank_1[542] = `ENABLE;
									en_1 = `ENABLE;
									we_1 = `ENABLE;
								end else if (cur_lru == 2'b01) begin
									for (loop = 0; loop < section_count; loop = loop + 1) begin
										if (!we) begin
											wdata_bank_2[(loop+section_start)<<6 +: 64] =
												data_l2_o[(loop+section_start)<<6 +: 64];
										end
									end
									wdata_bank_2[540:512] = tag_addr;
									wdata_bank_2[541] = `DISABLE;
									wdata_bank_2[542] = `ENABLE;
									en_2 = `ENABLE;
									we_2 = `ENABLE;
								end else if (cur_lru == 2'b10) begin
									for (loop = 0; loop < section_count; loop = loop + 1) begin
										wdata_bank_3[(loop+section_start)<<6 +: 64] =
											data_l2_o[(loop+section_start)<<6 +: 64];
									end
									wdata_bank_3[540:512] = tag_addr;
									wdata_bank_3[541] = `DISABLE;
									wdata_bank_3[542] = `ENABLE;
									en_3 = `ENABLE;
									we_3 = `ENABLE;
								end else begin
									for (loop = 0; loop < section_count; loop = loop + 1) begin
										wdata_bank_4[(loop+section_start)<<6 +: 64] =
											data_l2_o[(loop+section_start)<<6 +: 64];
									end
									wdata_bank_4[540:512] = tag_addr;
									wdata_bank_4[541] = `DISABLE;
									wdata_bank_4[542] = `ENABLE;
									en_4 = `ENABLE;
									we_4 = `ENABLE;
								end
							end else begin
								if (hit_way1) begin
									wdata_bank_1[511:0] = d_cache_wr_i;
									wdata_bank_1[540:512] = tag_addr;
									wdata_bank_1[541] = `ENABLE;
									wdata_bank_1[542] = `ENABLE;
									en_1 = `ENABLE;
									we_1 = `ENABLE;
								end else if (hit_way2) begin
									wdata_bank_2[511:0] = d_cache_wr_i;
									wdata_bank_2[540:512] = tag_addr;
									wdata_bank_2[541] = `ENABLE;
									wdata_bank_2[542] = `ENABLE;
									en_2 = `ENABLE;
									we_2 = `ENABLE;
								end else if (hit_way3) begin
									wdata_bank_3[511:0] = d_cache_wr_i;
									wdata_bank_3[540:512] = tag_addr;
									wdata_bank_3[541] = `ENABLE;
									wdata_bank_3[542] = `ENABLE;
									en_3 = `ENABLE;
									we_3 = `ENABLE;
								end else begin
									wdata_bank_4[511:0] = d_cache_wr_i;
									wdata_bank_4[540:512] = tag_addr;
									wdata_bank_4[541] = `ENABLE;
									wdata_bank_4[542] = `ENABLE;
									en_4 = `ENABLE;
									we_4 = `ENABLE;
								end
							end
							flushed = `ENABLE;
							next_state = DONE;
						end else begin
							// backup for late use
							for (loop = 0; loop < section_count; loop = loop + 1) begin
								data_l2_o[(loop+section_start)<<6 +: 64] =
									rd_data_i[(loop+section_start)<<6 +: 64];
							end

							if (cur_lru == 2'b00) begin
								// the only write chance is to flush
								if (rdata_bank_1[541]) begin
									next_state = FLUSH;
								end else begin
									if (!we) begin
										for (loop = 0; loop < section_count; loop = loop + 1) begin
											wdata_bank_1[(loop+section_start)<<6 +: 64] =
												rd_data_i[(loop+section_start)<<6 +: 64];
										end
										wdata_bank_1[541] = `DISABLE;
									end else begin
										wdata_bank_1[511:0] = d_cache_wr_i;
										wdata_bank_1[541] = `ENABLE;
									end
									wdata_bank_1[540:512] = tag_addr;
									wdata_bank_1[542] = `ENABLE;
									en_1 = `ENABLE;
									we_1 = `ENABLE;
									next_state = DONE;
								end
							end else if (cur_lru == 2'b01) begin
								if (rdata_bank_2[541]) begin
									next_state = FLUSH;
								end else begin
									if (!we) begin
										for (loop = 0; loop < section_count; loop = loop + 1) begin
											wdata_bank_2[(loop+section_start)<<6 +: 64] =
												rd_data_i[(loop+section_start)<<6 +: 64];
										end
										wdata_bank_2[541] = `DISABLE;
									end else begin
										wdata_bank_2[511:0] = d_cache_wr_i;
										wdata_bank_2[541] = `ENABLE;
									end
									wdata_bank_2[540:512] = tag_addr;
									wdata_bank_2[542] = `ENABLE;
									en_2 = `ENABLE;
									we_2 = `ENABLE;
									next_state = DONE;
								end
							end else if (cur_lru == 2'b10) begin
								if (rdata_bank_3[541]) begin
									next_state = FLUSH;
								end else begin
									if (!we) begin
										for (loop = 0; loop < section_count; loop = loop + 1) begin
											wdata_bank_3[(loop+section_start)<<6 +: 64] =
												rd_data_i[(loop+section_start)<<6 +: 64];
										end
										wdata_bank_3[541] = `DISABLE;
									end else begin
										wdata_bank_3[511:0] = d_cache_wr_i;
										wdata_bank_3[541] = `ENABLE;
									end
									wdata_bank_3[540:512] = tag_addr;
									wdata_bank_3[542] = `ENABLE;
									en_3 = `ENABLE;
									we_3 = `ENABLE;
									next_state = DONE;
								end
							end else begin
								if (rdata_bank_4[541]) begin
									next_state = FLUSH;
								end else begin
									if (!we) begin
										for (loop = 0; loop < section_count; loop = loop + 1) begin
											wdata_bank_4[(loop+section_start)<<6 +: 64] =
												rd_data_i[(loop+section_start)<<6 +: 64];
										end
										wdata_bank_4[541] = `DISABLE;
									end else begin
										wdata_bank_4[511:0] = d_cache_wr_i;
										wdata_bank_4[541] = `ENABLE;
									end
									wdata_bank_4[540:512] = tag_addr;
									wdata_bank_4[542] = `ENABLE;
									en_4 = `ENABLE;
									we_4 = `ENABLE;
									next_state = DONE;
								end
							end
						end
					end	else if (mem_req_o) begin
						mem_req_o = `DISABLE;
						next_state = COMM;
					end
				end

				FLUSH: begin
					flushing = `ENABLE;
					mem_req_o = `ENABLE;
					we_o = `ENABLE;
					if (cur_lru == 2'b00) begin
						// the only write chance is to flush
						addr_o = {rdata_bank_1[540:512], set_idx, 6'h0};
						wr_data_o = rdata_bank_1[511:0];
					end else if (cur_lru == 2'b01) begin
						addr_o = {rdata_bank_2[540:512], set_idx, 6'h0};
						wr_data_o = rdata_bank_2[511:0];
					end else if (cur_lru == 2'b10) begin
						addr_o = {rdata_bank_3[540:512], set_idx, 6'h0};
						wr_data_o = rdata_bank_3[511:0];
					end else begin
						addr_o = {rdata_bank_4[540:512], set_idx, 6'h0};
						wr_data_o = rdata_bank_4[511:0];
					end
					wr_strb_o = 64'hffffffffffffffff;
					pulse_bits_o = `CACHE_BITS;
					next_state = COMM;
				end

				WRITE: begin
					if (hit_way1) begin
						// the only write chance is to flush
						cur_lru = 2'b00;
						if (rdata_bank_1[541]) begin
							flushing = `ENABLE;
							mem_req_o = `ENABLE;
							we_o = `ENABLE;
							addr_o = {rdata_bank_1[540:512], set_idx, 6'h0};
							wr_data_o = rdata_bank_1[511:0];
							wr_strb_o = 64'hffffffffffffffff;
							pulse_bits_o = `CACHE_BITS;
							next_state = COMM;
						end else begin
							en_1 = `ENABLE;
							we_1 = `ENABLE;
							wdata_bank_1 = {2'b11, tag_addr, d_cache_wr_i};
							next_state = DONE;
						end
					end else if (hit_way2) begin
						cur_lru = 2'b01;
						if (rdata_bank_2[541]) begin
							flushing = `ENABLE;
							mem_req_o = `ENABLE;
							we_o = `ENABLE;
							addr_o = {rdata_bank_2[540:512], set_idx, 6'h0};
							wr_data_o = rdata_bank_2[511:0];
							wr_strb_o = 64'hffffffffffffffff;
							pulse_bits_o = `CACHE_BITS;
							next_state = COMM;
						end else begin
							en_2 = `ENABLE;
							we_2 = `ENABLE;
							wdata_bank_2 = {2'b11, tag_addr, d_cache_wr_i};
							next_state = DONE;
						end
					end else if (hit_way3) begin
						cur_lru = 2'b10;
						if (rdata_bank_3[541]) begin
							flushing = `ENABLE;
							mem_req_o = `ENABLE;
							we_o = `ENABLE;
							addr_o = {rdata_bank_3[540:512], set_idx, 6'h0};
							wr_data_o = rdata_bank_3[511:0];
							wr_strb_o = 64'hffffffffffffffff;
							pulse_bits_o = `CACHE_BITS;
							next_state = COMM;
						end else begin
							en_3 = `ENABLE;
							we_3 = `ENABLE;
							wdata_bank_3 = {2'b11, tag_addr, d_cache_wr_i};
							next_state = DONE;
						end
					end else begin
						cur_lru = 2'b11;
						if (rdata_bank_4[541]) begin
							flushing = `ENABLE;
							mem_req_o = `ENABLE;
							we_o = `ENABLE;
							addr_o = {rdata_bank_4[540:512], set_idx, 6'h0};
							wr_data_o = rdata_bank_4[511:0];
							wr_strb_o = 64'hffffffffffffffff;
							pulse_bits_o = `CACHE_BITS;
							next_state = COMM;
						end else begin
							en_4 = `ENABLE;
							we_4 = `ENABLE;
							wdata_bank_4 = {2'b11, tag_addr, d_cache_wr_i};
							next_state = DONE;
						end
					end
				end

				DONE: begin
					req_done = `ENABLE;
					if (cur_going == 2'b01) begin
						queue[0] = `DISABLE;
					end
					if (cur_going == 2'b10) begin
						queue[1] = `DISABLE;
					end
					flushing = `DISABLE;
					flushed = `DISABLE;
					en_1 = `DISABLE;
					en_2 = `DISABLE;
					en_3 = `DISABLE;
					en_4 = `DISABLE;
					we_1 = `DISABLE;
					we_2 = `DISABLE;
					we_3 = `DISABLE;
					we_4 = `DISABLE;
					next_state = IDLE;
				end

				default: begin
					next_state = IDLE;
				end
			endcase
		end
	end

	always @(posedge clk) begin
		if (rst_n == `RESET_ENABLE) begin
			ls_going <= `DISABLE;
		end else if (queue != 2'b00 && cur_state != IDLE) begin
			ls_going <= `ENABLE;
		end else begin
			ls_going <= `DISABLE;
		end
	end

	always @(posedge clk) begin
		if (cur_state == DONE) begin
			lru[set_idx] <= lru[set_idx] + 1;
		end
	end

endmodule
